﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using XcoAppSpaces.Contracts.Transport;
using System.Net;
using XcoAppSpaces.Transport.Sockets.Auth.Contract;
using XcoAppSpaces.Transport.Sockets.Auth;
using System.Threading;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Logging;

namespace XcoAppSpaces.Transport.Sockets
{
	internal class TCPClient : IDisposable
	{
		private readonly IXcoLogger log;
		private readonly TCPServer server;
		private readonly string remoteAddress;
		private readonly string localAddress;
		private readonly SecuritySettings securitySettings;
		private readonly TransferHelper transferHelper;
		private TcpClient client;
		private bool clientManagedByServer;
		private NetworkStream sendStream;

		private DateTime lastSendTime = DateTime.Now;

		public bool IsInitialized { get; set; }
		public Queue<XcoMessage> WaitingMessages { get; private set; }

		public TCPClient(TCPServer server, string remoteAddress, string localAddress, SecuritySettings securitySettings,
		                 IXcoLogger log)
		{
			IsInitialized = false;
			WaitingMessages = new Queue<XcoMessage>();

			this.server = server;
			this.remoteAddress = remoteAddress;
			this.localAddress = localAddress;
			this.securitySettings = securitySettings;
			this.log = log;

			transferHelper = new TransferHelper();
		}

		public void Initialize()
		{
			try
			{
				client = server.GetActiveTcpClient(remoteAddress);
				clientManagedByServer = (client != null);
				if (!clientManagedByServer)
				{
					client = GetTcpClient(remoteAddress, server.LocalEndPoint);
					AuthenticateTcpClient(client, securitySettings);
					server.AddReceiver(client, () => lastSendTime = DateTime.Now);
				}
				sendStream = client.GetStream();

				TransferWaitingMessages();
			}
			catch (Exception ex)
			{
				FailWaitingMessages(ex);
				throw;
			}

		}

		private void TransferWaitingMessages()
		{
			lock (WaitingMessages)
			{
				if (WaitingMessages.Count > 0)
				{
					log.Debug("Connection to " + remoteAddress + " opened successfully - transferring " + WaitingMessages.Count + " waiting messages.");
					while (WaitingMessages.Count > 0)
					{
						var msg = WaitingMessages.Dequeue();
						try
						{
							TransferMsg(msg);
						}
						catch (Exception) //when an exception occurs during transfer of the waiting messages, all further messages should be marked as failed (therefore also put the currently failed message back in the queue)
						{
							WaitingMessages.Enqueue(msg);
							throw;
						}
						
					}
				}
				IsInitialized = true;
			}
		}

		private void FailWaitingMessages(Exception ex)
		{
			lock (WaitingMessages)
			{
				if (WaitingMessages.Count > 0)
				{
					log.Warn("Error opening connection to " + remoteAddress + " - marking " + WaitingMessages.Count + " waiting messages as failed.");
					var commEx = new XcoCommunicationException("Error sending Message to Address \"" + remoteAddress + "\" : " + ex.Message, ex);
					while (WaitingMessages.Count > 0)
					{
						WaitingMessages.Dequeue().RaiseTransferFailedEvent(commEx);
					}
				}
				IsInitialized = true; //set this to true so that no more messages will be put into the waiting queue - the next message trying to be sent will trigger a recreate of the tcp client
			}
		}

		public DateTime LastSendTime
		{
			get { return lastSendTime; }
		}

		public void Send(XcoMessage msg)
		{
			//if client not initialized then add to waiting messages, otherwise send directly
			if (!IsInitialized)
			{
				lock (WaitingMessages)
				{
					if (!IsInitialized)
						WaitingMessages.Enqueue(msg);
					else
						TransferMsg(msg);
				}
			}
			else
				TransferMsg(msg);
		}

		private void TransferMsg(XcoMessage msg)
		{
			byte[] bytes = transferHelper.Convert(msg, localAddress);
			lock (this)
			{
				sendStream.Write(bytes, 0, bytes.Length);
			}
			lastSendTime = DateTime.Now;
			msg.RaiseTransferSuccessfulEvent();
			//when an error occurs, the transfer failed event cannot be raised here directly, because the message could still be successfully transferred with a retry
		}

		public void Close()
		{
			if (clientManagedByServer)
				return;
			if (client != null)
			{
				client.Client.Close();
				client.Close();
			}
		}

		public void Dispose()
		{
			Close();
		}

		private static TcpClient GetTcpClient(string remoteAddress, IPEndPoint localEndPoint)
		{
			string[] addressParts = remoteAddress.Split(':');
			if (addressParts.Length != 2)
				throw new XcoCommunicationException("Address \"" + remoteAddress + "\" is invalid - address must be in the format hostname:port or ipaddress:port!");
			string hostname = addressParts[0];
			int port = Convert.ToInt32(addressParts[1]);
#if !PocketPC
			//resolving the host address manually seems to be much faster than letting the TcpClient do this by handing over the string directly
			var client = new TcpClient();
			client.Connect(Dns.GetHostAddresses(hostname), port);
			return client; //new TcpClient(hostname, port);
#else
			var client = new TcpClient(new IPEndPoint(localEndPoint.Address, 0));
			client.Connect(hostname, port);
			return client; //return new TcpClient(hostname, port);
#endif
		}

		private void AuthenticateTcpClient(TcpClient client, SecuritySettings securitySettings)
		{
			if (securitySettings.AuthenticationKey == null)
				return;

			var handshake = DoClientHandshake.CreateDefaultInstance();
			handshake.Inject(securitySettings);
			var evt = new ManualResetEvent(false);
			bool connectionValid = false;
			handshake.Out_AuthenticatedConnection += _ => { connectionValid = true; evt.Set(); };
			handshake.Out_InvalidConnection += _ => { evt.Set(); };

			log.Debug("Starting TCP connection handshake");
			handshake.In_Connection(client);
			evt.WaitOne(securitySettings.SecondsAfterInvalidConnectionIsClosed * 2);
			if (!connectionValid)
			{
				log.Debug("TCP connection handshake invalid!");
				var endpointStr = client.Client.RemoteEndPoint.ToString();
				client.Client.Close();
				client.Close();
				throw new XcoCommunicationException("Connection to " + endpointStr + " could not be authenticated.");
			}
			else
				log.Debug("TCP connection handshake successful");
		}
	}
}
